//
// 自动滚动（拖拽靠近上下边缘时滚动视图）
//

import 'dart:async';
import 'dart:math' as math;

/// 调试开关：为 `true` 时可在外层基于此开关打印日志/可视化（本类未直接使用）。
const bool kAutoScrollDebug = true;

/// **AutoScroller**
///
/// 当指针（鼠标/触摸）在容器垂直方向靠近上下边缘时，
/// 以指数曲线映射距离为滚动速度，按固定 `tick` 周期触发回调以推进滚动。
///
/// - **无副作用原则**：本类不持有外部状态，仅通过回调读写滚动位置；
/// - **可重入**：重复调用 [ensureRunning] 会复用现有定时器，自动判断
///   何时停止；
/// - **稳定性**：内部对 `dt` 做了夹取，避免因掉帧导致的巨步长滚动；
///
/// ### 使用方法（伪代码）
/// ```dart
/// final scroller = AutoScroller();
/// // 在拖拽/选择手势 move 回调中：
/// scroller.ensureRunning(
///   pointerY: () => localPointerY,
///   viewportHeight: () => viewportH,
///   contentHeight: () => contentH,
///   getScrollY: () => scrollY,
///   applyScroll: (y) => setState(() => scrollY = y),
///   afterScrollTick: () => updateSelectionWhileAutoScrolling(),
/// );
/// // 在手势结束时：
/// scroller.stop();
/// ```
///
/// ### 速度曲线
/// - 当指针进入 `margin` 区域后才考虑滚动；
/// - 在 `deadZone` 内不滚动；从 `deadZone` 到 `margin` 的深度映射到 `t ∈ [0,1]`；
/// - 速度 `speed = lerp(minSpeed, maxSpeed, pow(t, exponent))`；
/// - 顶部方向为负（向上），底部方向为正（向下）。
class AutoScroller {
  AutoScroller({
    this.margin = 48.0,
    this.deadZone = 8.0,
    this.minSpeed = 1800.0,             // 默认 ~ 8 行/秒（行高约 22 时）
    this.maxSpeed = 18000.0,            // 默认 ~ 60–70 行/秒
    this.exponent = 10,
    this.tick = const Duration(milliseconds: 16),
  }) : assert(margin > 0),
        assert(deadZone >= 0),
        assert(minSpeed >= 0),
        assert(maxSpeed >= 0),
        assert(exponent > 0);

  /// 触发滚动的边缘宽度（px）。
  ///
  /// 指针位于 `[0, margin]`（顶部）或 `[viewportHeight - margin, viewportHeight]`（底部）
  /// 时视为进入边缘区，可能触发滚动。
  final double margin;

  /// 边缘内“死区”（px）。
  ///
  /// 指针进入 `margin` 但未超过 `deadZone` 时不滚动，避免轻微靠近即触发。
  final double deadZone;

  /// 最小/最大滚动速度（px/s）。
  ///
  /// 实际速度在两者之间插值；会自动按 `min/max` 排序，避免错误配置。
  final double minSpeed;
  final double maxSpeed;

  /// 速度曲线指数。
  ///
  /// 越大则越“靠近边缘才提速”，形成更陡的尾部加速。
  final double exponent;

  /// 驱动定时器的目标间隔。
  final Duration tick;

  Timer? _timer;
  DateTime? _lastTick;

  /// 当前是否在自动滚动。
  bool get isRunning => _timer != null;

  /// 根据当前指针/视口/内容信息，决定是否启动或维持自动滚动；不满足条件则停止。
  ///
  /// - [pointerY]：返回当前指针的 **本地 y 坐标**；
  /// - [viewportHeight]：返回视口高度（px）；
  /// - [contentHeight]：返回内容总高度（px）；
  /// - [getScrollY]：返回当前的滚动偏移量（px）；
  /// - [applyScroll]：设置新的滚动偏移量（建议在 `setState` 中调用）；
  /// - [afterScrollTick]：每次应用滚动后调用（可用于在拖拽选择时更新选区等）。
  void ensureRunning({
    required double Function() pointerY,
    required double Function() viewportHeight,
    required double Function() contentHeight,
    required double Function() getScrollY,
    required void Function(double newScrollY) applyScroll,
    void Function()? afterScrollTick,
  }) {
    final py = pointerY();
    final vh = viewportHeight();
    final dist = _edgeDistance(py, vh);

    // 未进入边缘或未超过死区 -> 停止
    if (!_shouldScroll(dist)) {
      stop();
      return;
    }

    // 启动或复用定时器
    _timer ??= Timer.periodic(tick, (_) {
      final now = DateTime.now();
      final last = _lastTick ?? now;
      _lastTick = now;

      // 实测 dt（秒），夹到 [0.5 * tick, 2.0 * tick]，避免掉帧导致巨步长
      double dt = now.difference(last).inMicroseconds / 1e6;
      final double target = tick.inMicroseconds / 1e6;
      dt = dt.clamp(target * 0.5, target * 2.0);

      final double y = pointerY();
      final double viewH = viewportHeight();
      final double contH = contentHeight();

      // 条件无效或无需滚动 -> 停止
      final double d = _edgeDistance(y, viewH);
      if (!_shouldScroll(d) || viewH <= 0 || contH <= viewH) {
        stop();
        return;
      }

      // 归一化深度 t ∈ [0,1]（扣除 deadZone）
      final double ramp = (margin - deadZone).clamp(1e-6, double.infinity);
      final double t = ((d - deadZone) / ramp).clamp(0.0, 1.0);

      // 速度（px/s）：在 [smin, smax] 内插值 + 指数曲线
      final double smin = math.min(minSpeed, maxSpeed);
      final double smax = math.max(minSpeed, maxSpeed);
      final double speed = smin + (smax - smin) * math.pow(t, exponent).toDouble();

      // 方向：上负下正
      final int dir = _scrollDirection(y, viewH);
      final double dy = dir * speed * dt;

      // 计算边界并夹取
      final double maxScroll = math.max(0.0, contH - viewH);
      if (maxScroll <= 0) {
        stop();
        return;
      }

      final double oldScroll = getScrollY();
      final double newScroll = (oldScroll + dy).clamp(0.0, maxScroll.toDouble());

      applyScroll(newScroll);
      afterScrollTick?.call();
    });
  }

  /// 停止自动滚动并重置内部定时器时间戳。
  void stop() {
    _timer?.cancel();
    _timer = null;
    _lastTick = null;
  }

  // 返回距离最近边缘的“进入边缘区”的深度（px）。
  // 顶部：dist = margin - y；底部：dist = y - (vh - margin)；中间区域 dist < 0。
  double _edgeDistance(double y, double vh) {
    if (y <= margin) return margin - y;                      // 顶部
    if (y >= vh - margin) return y - (vh - margin);          // 底部
    return -1.0; // 未进入边缘区
  }

  // 是否应滚动：进入边缘区且超过 deadZone。
  bool _shouldScroll(double edgeDist) => edgeDist >= deadZone;

  // 方向：顶部向上(-1)，底部向下(+1)，中间 0（理论上不会被调用）。
  int _scrollDirection(double y, double vh) {
    if (y <= margin) return -1;
    if (y >= vh - margin) return 1;
    return 0;
  }
}
